package lumis.portal.dao.hibernate;

import lumis.portal.PortalObjectAlreadyExistsException;
import lumis.portal.PortalException;
import lumis.portal.PortalObjectNotFoundException;
import lumis.portal.dao.DaoException;
import lumis.util.ITransaction;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;

/**
 * Implements the generic CRUD data access operations using Hibernate APIs.
 * <p>
 * The instances returned by this class do not automatically persist changes
 * to its state. The method {@link #update(Object, ITransaction)} must be
 * called to do it.
 * <p>
 * To write a DAO, subclass and parameterize this class with your persistent class.
 * Of course, assuming that you have a traditional 1:1 approach for Entity:DAO design.
 * <p>
 * You have to inject the <tt>Class</tt> object of the persistent class to construct a DAO.
 *
 * @since 4.0.11
 * @version $Revision: 9649 $ $Date: 2008-08-07 16:06:56 -0300 (Thu, 07 Aug 2008) $
 */
public abstract class GenericHibernateManualUpdateDao<T, ID extends Serializable>
{
    private Class<T> persistentClass;

    protected GenericHibernateManualUpdateDao(Class<T> persistentClass) 
    {
        this.persistentClass = persistentClass;
    }
    
    protected abstract ID getIdentifier(T bean);

    /**
     * Returns the class whose persistence this DAO controls. 
     */
    protected Class<T> getPersistentClass() 
	{
        return persistentClass;
    }

    /**
     * Returns the bean with the given id. The bean is
     * set as read-only, so changes on it won't persist automatically.
     * @param id the bean's id.
	 * @param transaction the transaction for persistence access. Must be
	 * a {@link ITransactionHibernate}.
     * @return the bean.
     * @throws PortalObjectNotFoundException if the corresponding bean does not exist in the persistence.
     */
	@SuppressWarnings("unchecked")
	protected T get(ID id, ITransaction transaction) throws PortalObjectNotFoundException, PortalException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
		T bean = (T)session.get(getPersistentClass(), id);
		if (bean == null)
			throw new PortalObjectNotFoundException("STR_RESOURCE_NOT_FOUND;" + id);
		session.setReadOnly(bean, true); // will not automatically persist changes
		return bean;
	}

	/**
	 * Adds the bean to the persistence layer. 
	 * @param bean the bean to be added.
	 * @param transaction the transaction for persistence access.
	 * @throws PortalException
	 */
	protected void add(T bean, ITransaction transaction) throws PortalException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();

		try
		{
			ID id = getIdentifier(bean);
			if (id != null)
			{
				get(id, transaction);
				throw new PortalObjectAlreadyExistsException("STR_RESOURCE_ALREADY_EXISTS;" + id.toString());
			}
		}
		catch (PortalObjectNotFoundException e)
		{
			// expected exception
		}
		
		if (session.contains(bean))
		{
			// the bean must be evicted before persisting if the session contains it.
			// probably the same bean instance is being utilized for several add operations.
			session.evict(bean);
		}

		session.persist(bean);
		session.flush();
		session.setReadOnly(bean, true); // will not automatically persist changes
	}

	protected void update(T bean, ITransaction transaction) throws PortalException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
		
		if (session.contains(bean))
		{
			// eliminate the read-only bean (supposed to be taken by the get method) from session and detach it
			session.evict(bean);
		}
		else
		{
			// if the bean does not exist in persistence, throw exception
			Object existCheck = session.get(getPersistentClass(), getIdentifier(bean));
			if (existCheck == null)
				throw new PortalObjectNotFoundException("Update on non-existant bean: " + getPersistentClass() + ", id=" + getIdentifier(bean));
			session.evict(existCheck);
		}
			
		// update changes in bean on the persistence
		session.merge(bean);
		session.flush();
	}
	
	protected void addOrUpdate(T bean, ITransaction transaction) throws PortalException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
		if (session.contains(bean))
		{
			// eliminate the read-only bean (supposed to be taken by the get method) from session and detach it
			session.evict(bean);
		}
		else
		{
			ID beanId = getIdentifier(bean);
			if (beanId != null)
			{
				// if a bean with the same identifier exists in memory, clear it.
				Object existCheck = session.get(getPersistentClass(), beanId);
				if (existCheck != null)
					session.evict(existCheck);
			}
		}
		
		// update changes in bean on the persistence (or add it if it does not exist)
		session.merge(bean);
		session.flush();
	}

	/**
	 * Deletes the bean with the given id from persistence.
	 * @param id the bean's id.
	 * @param transaction the transaction for persistence access. Must be
	 * a {@link ITransactionHibernate}.
	 * @return true if the bean was deleted, or false if it did not exist
	 */
	@SuppressWarnings("unchecked")
	protected boolean delete(ID id, ITransaction transaction) throws PortalException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
		T bean = (T)session.get(getPersistentClass(), id);
		if (bean != null)
		{
			session.delete(bean);
			session.flush();
			return true;
		}
		else
			return false;
	}
	
	/**
	 * Evicts the bean with the given id from the second-level cache.
	 * @param id the bean identifier.
	 * @param transaction the transaction for persistence access.
	 * @since 4.0.11
	 */
	protected void evict(ID id, ITransaction transaction) throws PortalException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
		session.getSessionFactory().evict(getPersistentClass(), id);
	}
	
	/** Sets a collection of beans to read-only state. */
	protected void setReadOnly(ITransaction transaction, Collection<T> beans) throws DaoException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
        for (T bean: beans)
        {
        	session.setReadOnly(bean, true);
        }		
	}
	
	/** Convenience method for subclasses. All returned entities are returned in read-only state. */
    @SuppressWarnings("unchecked")
	protected List<T> findByCriteria(ITransaction transaction, Criterion[] criterions, Order[] orders) throws DaoException
	{
		ITransactionHibernate txHib = (ITransactionHibernate)transaction;
		Session session = txHib.getSession();
        Criteria crit = session.createCriteria(getPersistentClass());
        if (criterions != null)
        {
            for (Criterion c : criterions) 
                crit.add(c);       	
        }
        if (orders != null)
        {
            for (Order order: orders)
            	crit.addOrder(order);
        }
        List<T> beans = crit.list();
        setReadOnly(transaction, beans);
        return beans;
   }

    /** Convenience method for subclasses. All returned entities are returned in read-only state. */
    protected List<T> findByCriteria(ITransaction transaction, Criterion... criterions) throws DaoException
	{
    	return findByCriteria(transaction, criterions, null);
   }
}
